<?php
/**
 * Smart Library - Smart Framework
 * http://smart-framework.highet.eu
 *
 * @category   SmartL
 * @package    SmartL_Application
 * @copyright  Copyright (c) 2005-2008 Smart Framework team - Stepan Sindelar, Vojtech Vit
 * @license    http://www.opensource.org/licenses/bsd-license.php	New BSD License
 */

/**
 * @see Zend_Auth
 */
require_once 'Zend/Auth.php';
/**
 * @see Zend_Controller_Front
 */
require_once 'Zend/Controller/Front.php';
/**
 * @see Zend_Layout
 */
require_once 'Zend/Layout.php';
/**
 * @see Zend_Locale
 */
require_once 'Zend/Locale.php';
/**
 * @see SmartL_Application_Config
 */
require_once 'SmartL/Application/Config.php';
/**
 * @see SmartL_Application_Config_LanguageInfo
 */
require_once 'SmartL/Application/Config/LanguageInfo.php';
/**
 * @see SmartL_Application_Login_Interface
 */
require_once 'SmartL/Application/Login/Interface.php';
/**
 * @see SmartL_Application_Plugin_InitFramework
 */
require_once 'SmartL/Application/Plugin/InitFramework.php';
/**
 * @see SmartL_Zend_View
 */
require_once 'SmartL/Zend/View.php';
/**
 * @see SmartL_Acl
 */
require_once 'SmartL/Acl.php';
/**
 * @see SmartL_DatabaseManager
 */
require_once 'SmartL/DatabaseManager.php';




/**
 * Singleton class used to initialize all basic application resources and functions.
 * It also behaves as a central storage for all application global
 * classes and configurations (this functionality is given by
 * SmartL_Application_Config class which this class derives from).
 *
 *  @todo 26: Podpora cachovani na všech úrovních
 *
 * @package SmartL_Application
 */
class SmartL_Application extends SmartL_Application_Config
{
    /**
     * Name of application and module configuration files in configurations
     * directory.
     */
    const CONFIG_FILENAME = 'Config.xml';
    /**
     * Default module name
     */
    const MODULE_DEFAULT = 'Default';
    /**
     * Identifier used to recognize languages in URLs
     */
    const URL_LANGUAGE_ID = 'languageId';
    /**
     * Value used in URL to get the default language
     */
    const URL_LANGUAGE_DEFAULT = 'default';

    /**
     * Instance of this class. Used by Singleton pattern.
     *
     * @var SmartL_Application
     */
    private static $_instance = null;
    /**
     * Instance of Zend MVC Front Controller
     *
     * @var Zend_Controller_Front
     */
    private $_frontController = null;
    /**
     * Actual module info class instance
     *
     * @var SmartL_Application_ModuleInfo
     */
    private $_actualModule = null;
    /**
     * Application base dir path
     *
     * @var string
     */
    private $_basePath;
    /**
     * Relative path to configurations dir from application base dir
     *
     * @var string
     */
    private $_configPath;
    /**
     * Instance of class that performs login logic. It implements
     * SmartL_Application_Login_Interface. SmartL_Application takes care of
     * calling method login() and of authorization using getRole().
     *
     * @var SmartL_Application_Login_Interface
     */
    private $_login = null;
    /**
     * User defined access permissions of the actual module.
     *
     * @var array
     */
    private $_allow = array();
    /**
     * User defined access restrictions of the actual module.
     *
     * @var array
     */
    private $_deny = array();
    /**
     * Preferred languages of current user. The most preferred language has
     * index number 0, the least preferred is the last one.
     *
     * @var array of SmartL_Application_LanguageInfo
     */
    private $_languages = array();
    /**
     * Cache manager instance.
     *
     * @var SmartL_Cache_Manager
     */
    private $_cacheManager;

    /**
     * Class constructor is protected, because it implements the
     * Singleton design pattern. Use getInstance() method instead.
     *
     */
    protected function __construct ()
    {
        parent::__construct();
        $this->_frontController = Zend_Controller_Front::getInstance();
    }
    /**
     * Singleton constructor. Returns the only instance of this class.
     *
     * @return SmartL_Application
     */
    public static function getInstance ()
    {
        if (!self::$_instance instanceof self) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }
    /**
     * Returns actual module info.
     * Returns null if method initModule() wasn't yet called.
     *
     * @return SmartL_Application_Config_ModuleInfo
     */
    public function getActualModule ()
    {
        return $this->_actualModule;
    }
    /**
     * Returns actually used protocol (like 'http://', 'https://')
     *
     * @todo 18 : Získání aktuálního protokolu
     * @return string Protocol
     */
    public function getActualProtocol()
    {
        return 'http://';
    }
    /**
     * Returns path to application root (base) directory.
     *
     * @return string Base directory path
     */
    public function getBasePath ()
    {
        return $this->_basePath;
    }
    /**
     * Returns instance of login class.
     *
     * @return SmartL_Application_Login_Interface
     */
    public function getLoginInstance()
    {
    	return $this->_login;
    }
    /**
     * Returns relative path to configurations directory from application base
     * directory.
     *
     * @return string Relative path to configurations dir from application base dir
     */
    public function getConfigPath ()
    {
        return $this->_configPath;
    }
    /**
     * Returns path to cofigurations dir.
     *
     * @return string Full path to configurations dir
     */
    public function getConfigDir ()
    {
        return realpath($this->getBasePath() . DIRECTORY_SEPARATOR . $this->getConfigPath());
    }
    /**
     * Returns user defined access permissions of the actual module.
     * These rights can be set on-the-fly, unlike ConfigAccessPermissions
     * that can be set only in the configuration.
     *
     * @var array
     */
    public function getAccessPermissions ()
    {
        return array_merge($this->_allow, $this->getConfigAccessPermissions());
    }
    /**
     * Returns user defined access restrictions of the actual module.
     * These rights can be set on-the-fly, unlike ConfigAccessRestrictions
     * that can be set only in the configuration.
     *
     * @var array
     */
    public function getAccessRestrictions ()
    {
        return array_merge($this->_deny, $this->getConfigAccessRestrictions());
    }
    /**
     * Returns array of preferred languages.
     * The most preferred language has index number 0, the least preferred is
     * the last one (default language).
     *
     * @return array of SmartL_Application_LanguageInfo
     */
    public function getLanguages ()
    {
        return $this->_languages;
    }
    /**
     * Sets the most preferred language for translations.
     *
     * @param SmartL_Application_Config_LanguageInfo $lang Language id string.
     * @param boolean $replaceCurrent Replace current most preferred language
     * @return SmartL_Application
     */
    public function setUserLanguage ( SmartL_Application_Config_LanguageInfo $langId, $replaceCurrent = true)
    {
        if ( isset($this->_languages[0]) and $this->_languages[0]->equals($langId) ) {
            return $this;
        }
        if ($replaceCurrent) {
            $this->_languages[0] = $langId;
        }
        else {
            $this->_languages = array_merge(array($langId),$this->_languages);
        }
        return $this;
    }
    /**
     * Sets the most preferred language (and culture) plus the default culture
     * of the same language.
     * It is used to ensure that if translation of the most preferred
     * culture wasn't found, default culture of the same language will be tried
     * afterwards.
     *
     * @param SmartL_Application_Config_LanguageInfo $language Most preferred language (with culture) infoclass
     */
    public function setUserLanguageWithDefaultCulture( SmartL_Application_Config_LanguageInfo $language ) {
        if ( $this->isSupportedLanguage($language, false) ) {
            //default culture of this language
            $defaultCult = $this->getDefaultCulture( $language->getLanguageId() );
            $this->setUserLanguage($defaultCult, false);
            if ( !$defaultCult->equals($language) and $this->isSupportedLanguage($language) ) {
                //the language
                $this->setUserLanguage($language, false);
            }
        }
        return $this;
    }
    /**
     * Returns role of current logged user.
     * It calls method getRole of instance of SmartL_Application_Login_Interface that was
     * given trought setLoginClass and returns its result.
     *
     * @return array[numeric]=>string
     */
    public function getUserRoles()
    {
        if (isset($this->_login)) {
            return $this->_login->getUserRoles();
        }
        else {
            return array(SmartL_Application_Login_Interface::LOGIN_ANONYMOUS);
        }
    }
    /**
     * Returns the cache manager.
     *
     * @return SmartL_Zend_Cache_Manager
     */
    public function getCacheManager()
    {
    	if ( $this->_cacheManager === null ) {
    		require_once 'SmartL/Zend/Cache/Manager.php';
    		$this->_cacheManager = new SmartL_Zend_Cache_Manager();
    	}
    	return $this->_cacheManager;
    }
    /**
     * Initializes application configuration, front controller, paths etc.
     *
     * @param string $basePath   Path to application base dir
     * @param string $configPath Relative path to configurations dir from
     *                           application base dir
     * @param SmartL_Application_Login_Interface $login
     * @return SmartL_Application
     */
    public function init ($basePath, $configPath, $login=null)
    {
        $this->_basePath   = rtrim($basePath, '\\/');
        $this->_configPath = trim($configPath, '\\/');
        $this->_login      = $login;

        $this->_loadAppConfiguration();
        if ( $this->isDebugMode() ) {
            require_once 'SmartL/Debug.php';
			SmartL_Debug::enable();
        }
        $this->_setupFrontController();
        $this->_setupLanguages();

        return $this;
    }
    /**
     * Starts dispatch.
     *
     * @return SmartL_Application
     */
    public function start ()
    {
        $this->_frontController->dispatch();
        return $this;
    }
    /**
     * Loads application configuration
     *
     * @uses SmartL_Application::CONFIG_FILENAME
     */
    protected function _loadAppConfiguration ()
    {
        if ( $this->_callPluginMethod("preLoadApplicationConfiguration") === false ) return;
        $this->load($this->getConfigDir() . DIRECTORY_SEPARATOR . self::CONFIG_FILENAME);
        $this->_callPluginMethod("postLoadApplicationConfiguration");
    }
    /**
     * Sets up front controller, modules and SmartL_Application InitFramework plugin
     *
     */
    protected function _setupFrontController ()
    {
        if ( $this->_callPluginMethod("preSetUpFrontController") === false ) {
           return;
        }
        $ds = DIRECTORY_SEPARATOR;

        // Set-up Front Controller
        $this->_frontController->throwExceptions($this->isDebugMode());
        //$this->_frontController->setParam("useDefaultControllerAlways", true);
        $baseUrl = $this->getBaseUrl();
        if ( !empty($baseUrl) ) $this->_frontController->setBaseUrl("/".$baseUrl);

        $this->_setUpRouter();

        // Set-up Modules
        foreach ($this->getModules() as $module) {
            $this->_frontController->addControllerDirectory($this->getbasePath() . $ds
             . $module->getControllersDir(), $module->getName());
        }
        $this->_frontController->setDefaultModule($this->getDefaultModule()->getName());

        // Set-up InitFramework plugin
        $this->_frontController->registerPlugin(new SmartL_Application_Plugin_InitFramework());
        $this->_callPluginMethod("postSetUpFrontController");
    }
    /**
     * Sets up router and routes.
     *
     */
    protected function _setUpRouter()
    {
        if ( isset($this->getConfig()->router) ) {

        	//class of Router
            $routerClass = "Zend_Controller_Router_Rewrite";
            if ( isset($this->getConfig()->router->class) ) {
                $routerClass = $this->getConfig()->router->class->getValue();
            }

            //Router's config
            if ( isset($this->getConfig()->router->routerConfig) ) {
            	$routerConfig = $this->getConfig()->router->routerConfig->toArray(false);
            }

            //load Router's class and make instance
            Zend_Loader::loadClass($routerClass);
            if ( isset($routerConfig) ) {
            	$router = new $routerClass($routerConfig);
            }
            else {
            	$router = new $routerClass();
            }

            //set up routes
            if ( isset($this->getConfig()->router->routes) ) {
                $data = $this->getConfig()->router->toArray();
                $router->addConfig(new Zend_Config($data['routes']));
            }

            $this->_frontController->setRouter($router);
        }
    }
    /**
     * Sets up preferred languages by config and user's browser.
     *
     */
    protected function _setupLanguages ()
    {
        if ( $this->_callPluginMethod("preSetUpLanguages") === false ) {
           return;
        }
        $this->_languages = array($this->getDefaultLanguage()); // Default from configuration
        
        try {
	        $browser = new Zend_Locale(Zend_Locale::BROWSER);
	        $browserId = $browser->getLanguage();
	        $data = explode("-",$browserId);
	        if ( count($data) > 0 ) {
	            $language = new SmartL_Application_Config_LanguageInfo();
	            $language->setLanguageId($data[0]);
	            if ( isset($data[1]) ) {
	                $language->setCultureId($data[1]);
	            }
	            $this->setUserLanguageWithDefaultCulture($language);
	        }
        }
        catch (Zend_Locale_Exception $e)
        {
        	/* If you want to run some scripts directly without web server, the Zend_Locale cannot
        	 * determine language frmo browser and throws an extension. The default language is in config
        	 * so we don't need to terminate application at this point.
        	 */
        }
        $this->_callPluginMethod("postSetUpLanguages");
    }
    /**
     * Opens database connections in Doctrine_Manager, sets the default
     * connection.
     *
     * @return bool True if one or more connections were found in configuration
     */
    protected function _openDbConnections ()
    {
        $additional = $this->_callPluginMethod("preOpenDbConnections");
        if ( $additional === false ) {
            return;
        }
        if ( !is_array($additional) ) {
            $additional = array();
        }

        $result = false;
        foreach ( array_merge($this->getConnectionsConfig(),$additional) as $name => $config) {
            SmartL_DatabaseManager::factory($config['adapter'])->openDbConnection($name, $config['config']);
            //default connection for given adapter?
            if ( isset($config['default']) and $config['default'] == 'true' ) {
            	SmartL_DatabaseManager::factory($config['adapter'])->setCurrentConnection($name);
            }
            /*if ( $this->isDebugMode() ) {
                SmartL_Profiler::getInstance()->addConnection($connection);
            }*/
        }
        
        $this->_callPluginMethod("postOpenDbConnections");
        return $result;
    }
    /**
     * Initializes cache adapters from config and sets up Zend_Translate cache.
     *
     */
	protected function _initCache()
    {
       	if ( $this->isCacheDisabled() ) {
       		$this->getCacheManager()->setCacheDisabled();
   		}    	
   		foreach ( $this->getCacheAdaptersConfig() as $adapter )
   		{
   			$this->getCacheManager()->addAdapter($adapter->getName(),$adapter,$adapter->isDisabled());
   		}
   		
   		if ( ($cacheName = $this->getZendTranslateCacheName()) !== null and 
   			!$this->isCacheDisabled() and 
   			!$this->getCacheManager()->getAdapter($cacheName)->isDisabled()) {
   			require_once 'Zend/Translate.php';
   			Zend_Translate::setCache($this->getCacheManager()->getAdapter($cacheName)->getWrappedObject());
   		}
    }
        
    /**
     * Authenticates user using set login class.
     *
     * @return Doctrine_Record User identity
     */
    protected function _authenticateUser ()
    {
        if ( $this->_callPluginMethod("preAuthenticateUser") == false ) {
           return;
        }
        if (!Zend_Auth::getInstance()->getIdentity() and isset($this->_login)) {
            $this->_login->login($this->_frontController->getRequest());
        }
        $this->_callPluginMethod("postAuthenticateUser");
    }
    /**
     * Authorizes user access (based on configuration).
     *
     */
    protected function _authorizeUser ()
    {
        if ( $this->_callPluginMethod("preAuthorizeUser") === false ) {
           return;
        }
        $authorized = true;
        if ( isset($this->_login) ) {
            $authorized = false;
            foreach ($this->_login->getUserRoles() as $role) {
                if ( SmartL_Acl::isAuthorized($role, $this->getAccessPermissions(), $this->getAccessRestrictions()) ) {
                    $authorized = true;
                    break;
                }
            }
        }

        if (!$authorized) {
            $newRequestInfo = $this->getAccessDeniedRequestInfo();
            if ($newRequestInfo != null) {
                $request = Zend_Controller_Front::getInstance()->getRequest();
                $request->setActionName($newRequestInfo->getAction());
                $request->setControllerName($newRequestInfo->getController());
                $request->setModuleName($newRequestInfo->getModule());
            } else {
                require_once 'SmartL/Application/AccessException.php';
                throw new SmartL_Application_AccessException("Access denied");
            }
        }
        $this->_callPluginMethod("postAuthorizeUser");
    }
    /**
     * Initializes actual module information.
     *
     * This method is called during the "routeShutdown" application run phase
     *
     * @param string $moduleName Actual module name
     * @uses SmartL_Application::MODULE_DEFAULT
     */
    protected function _initModule ($moduleName)
    {
        $result = $this->_callPluginMethod("preInitModule",array($moduleName));
        if ( $result === false ) {
           return;
        }
        if ( is_string($result[count($result)-1]) ) {
            $moduleName = $result[count($result)-1];
        }

        if ($moduleName == null or $moduleName == $this->_frontController->getDefaultModule()) {
            $this->_actualModule = $this->getDefaultModule();
        } else {
            $this->_actualModule = $this->getModule($moduleName);
        }

        $this->_loadModuleConfig($moduleName);
        $this->_callPluginMethod("postInitModule");
    }
    /**
     * Set up user preferences trough SmartL_Application_Login_UserPreferences_Interface.
     *
     */
    protected function _setUpUserPreferences()
    {
        $result = $this->_callPluginMethod("preSetUpUserPreferences");
        if ( $result === false ) {
            return;
        }
        if ( $result[count($result)-1] instanceof SmartL_Application_Login_UserPreferences_Interface ) {
            $data = $result[count($result)-1];
        }

        if ( !isset($data) and isset($this->_login) and Zend_Auth::getInstance()->getIdentity() ) {
            $data = $this->_login->getUserPreferences();
        }

        if ( isset($data) ) {
            //template
            $template = $data->getTemplateName();
            if ( $template != null ) $this->setActualTemplate($template);
            //skin
            $skin = $data->getSkinName();
            if ( $skin != null ) {
                if ( array_search($skin, $this->getActualTemplate()->getSkins()) === null ) {
                    require_once 'SmartL/Application/Exception.php';
                    throw new SmartL_Application_Exception("User preferred skin doesn't exist in current template.");
                }
                $this->getActualTemplate()->setActualSkin($skin);
            }
            //language
            $language = $data->getLanguage();
            if ( $language != null ) {
                $this->setUserLanguageWithDefaultCulture($language);
            }
        }

        $parameters = isset($data) ? array($data) : array();
        $this->_callPluginMethod("postSetUpUserPreferences", $parameters );
    }
    /**
     * Sets the url-language specification as the primary language indicator
     */
    protected function _setUpLanguageFromUrl()
    {
        $request = $this->_frontController->getRequest();
        $langId = $request->getParam(self::URL_LANGUAGE_ID);

        if ( $langId !== null && $langId != self::URL_LANGUAGE_DEFAULT ) {
            // Decode language id
            $langId = $this->_decodeLanguageId($request->getParam(self::URL_LANGUAGE_ID));

            $language = new SmartL_Application_Config_LanguageInfo($langId);

            $locales = Zend_Locale::getLocaleList();
            if ( isset($locales[$language->getZendId()]) ) {
                $this->setUserLanguageWithDefaultCulture($language);
            }
            else {
                require_once 'SmartL/Application/Exception.php';
                throw new SmartL_Zend_Controller_Router_Rewrite_Exception("Language given in url is not valid.");
            }
        }
    }
    /**
     * Loads module configuration and extends application configuration by
     * it.
     *
     * @param string $moduleName Module name
     * @return bool True if module configuration file was found
     */
    protected function _loadModuleConfig ($moduleName)
    {
        if ( $this->_callPluginMethod("preLoadModuleConfiguration") === false ) {
            return;
        }
        $ds = DIRECTORY_SEPARATOR;
        $filename = $this->getConfigDir() . $ds . $moduleName
                    . $ds . self::CONFIG_FILENAME;
        if (file_exists($filename)) {
            $this->extendBy($filename);
            $this->_callPluginMethod("postLoadModuleConfiguration");
            return true;
        }

        $this->_callPluginMethod("postLoadModuleConfiguration");
        return false;
    }
    /**
     * Starts Zend_Layout if no layout was set to the Actual Template before.
     *
     */
    protected function _startLayout ()
    {
        if ( $this->_callPluginMethod("preStartLayout") === false ) {
            $this->_callPluginMethod("postStartLayout", array(false));
            return;
        }
        if ( ($this->getActualTemplate() !== null) and $this->getActualTemplate()->hasLayout()) {
            Zend_Layout::startMvc();
        }
        $this->_callPluginMethod("postStartLayout", array(true));
    }
    /**
     * Helper for calling plugin methods.
     *
     * @param string $methodName Name of method to be called.
     * @param array $parameters array of parameters for method.
     * @return array[numeric]=>mixed|boolean False if result of one of callings was false, or array of
     * results of calls.
     */
    private function _callPluginMethod($methodName, array $parameters = array()) {
        $front = Zend_Controller_Front::getInstance();
        foreach ($front->getPlugins() as $plugin) {
            if ( $plugin instanceof SmartL_Application_Plugin_Interface ) {
                $result = call_user_func_array(array($plugin,$methodName) , $parameters);

                if ( $result === false ) return false;
                if ( $result !== null ) {
                    $results[] = $result;
                }
            }
        }
        return isset($results) ? $results : true;
    }

    /**
     * Helper method called by InitFramework Plugin on "routeStartup"
     * application run phase.
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function routeStartup (Zend_Controller_Request_Abstract $request)
    {}
    /**
     * Helper method called by InitFramework Plugin on "routeShutdown"
     * application run phase.
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function routeShutdown (Zend_Controller_Request_Abstract $request)
    {
        // Decode module name
        $moduleName = $this->_decodeModuleName($request->getParam($request->getModuleKey()));
        $request->setModuleName($moduleName);

        // Initialize module
        $this->_initModule($request->getModuleName());

        // Open Database Connection
        $this->_openDbConnections();

        // Authenticate (or log-in) User
        $this->_authenticateUser();

        // Authorize user access
        $this->_authorizeUser();

        // Set up user preferences
        $this->_setUpUserPreferences();

        // Set up language from url
        $this->_setUpLanguageFromUrl();
        
        // Set up cache
        $this->_initCache();
    }
    /**
     * Helper method called by InitFramework Plugin on "dispatchLoopStartup"
     * application run phase.
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function dispatchLoopStartup (Zend_Controller_Request_Abstract $request)
    {}
    /**
     * Helper method called by InitFramework Plugin on "preDispatch"
     * application run phase.
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function preDispatch (Zend_Controller_Request_Abstract $request)
    {
		// Start layout if it wasn't already started
	    $this->_startLayout();
    }
    /**
     * Helper method called by InitFramework Plugin on "postDispatch"
     * application run phase.
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function postDispatch (Zend_Controller_Request_Abstract $request)
    {}
    /**
     * Helper method called by InitFramework Plugin on "dispatchLoopShutdown"
     * application run phase.
     *
     */
    public function dispatchLoopShutdown ()
    {
    }
    /**
     * Renames language id with specified, lowercase culture (like cs-cz) to
     * correct ISO format with uppercase culture (like cs-CZ).
     *
     * @param string $langId Language id with lowercase culture
     * @return string Language id with uppercase culture
     */
    private function _decodeLanguageId($langId)
    {
        if (strpos($langId, '-') !== false) {
            $explode = explode('-', $langId);
            if (count($explode) == 2) {
                list ($langId, $cultureId) = $explode;
                return $langId . '-' . strtoupper($cultureId);
            }
        }
        require_once 'SmartL/Zend/Controller/Router/Rewrite/Exception.php';
        throw new SmartL_Zend_Controller_Router_Rewrite_Exception("Language given in url is not in valid format.");
    }
    /**
     * Renames module name with dashes (like some-module - used in URLs) to
     * it's correct form (like SomeModule)
     *
     * @param string $moduleName Module name with dashes
     * @return string Correct module name
     */
    private function _decodeModuleName($moduleName)
    {
        $words = explode('-', $moduleName);
        $moduleName = '';
        foreach ($words as $word) {
            $moduleName .= ucfirst($word);
        }
        return $moduleName;
    }
}